﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Xml;
using System.Globalization;
using System.Xml.Linq;

namespace LotroData
{
    /// <summary>
    /// Performs actions related to the on-disk file caching of LotroData XML streams. 
    /// </summary>
    public static class LotroCache
    {
        //Cached filename format: {name}_{world}_{timestamp}.xml
        private const string TimestampFormat = "yyyyMMddHHmm";

        /// <summary>
        /// Default Value for the Cache Time. Saved as the number of days, to be used with TimeSpan.FromDays()
        /// </summary>
        private const int CacheTimeDaysDefaultValue = 1;

        /// <summary>
        /// Gets the current cache path.
        /// </summary>
        /// <value>The on-disk cache path.</value>
        public static string CachePath { get; private set; }

        /// <summary>
        /// Gets or sets the cache time. 
        /// This is the maximum time the on-disk cache is used in preference to a new XML stream.
        /// </summary>
        /// <value>The cache time.</value>
        public static TimeSpan CacheTime { get; set; }

        /// <summary>
        /// Initializes the <see cref="LotroCache"/> class.
        /// </summary>
        static LotroCache()
        {
            CacheTime = TimeSpan.FromDays(CacheTimeDaysDefaultValue);

            var root = Environment.CurrentDirectory;

            ChangeCache(Path.Combine(root, "cache"), false);
        }

        /// <summary>
        /// Changes the cache path location
        /// </summary>
        /// <exception cref="ArgumentNullException">Thrown if the supplied path is null or empty.</exception>
        /// <param name="path">The new cache path.</param>
        /// <param name="deletePrevious">if set to <c>true</c> deletes the previous cache on a successful cache change.</param>
        public static void ChangeCache(string path, bool deletePrevious)
        {
            if (string.IsNullOrEmpty(path)) throw new ArgumentNullException("path");

            var previous = CachePath;

            try
            {
                if (!Directory.Exists(path))
                    Directory.CreateDirectory(path);

                CachePath = path;

                if (!string.IsNullOrEmpty(previous) && deletePrevious)
                    Directory.Delete(previous, true);
            }
            catch (IOException)
            {
                throw;
            }
        }

        /// <summary>
        /// Saves the supplied XML document to the cache directory
        /// </summary>
        /// <param name="name">The name, either character or guild.</param>
        /// <param name="world">The associated world.</param>
        /// <param name="element">The XML document.</param>
        public static void SaveXmlToCache(string name, LotroWorld world, XElement element)
        {
            if (string.IsNullOrEmpty(name)) throw new ArgumentNullException("name");
            if (world == LotroWorld.Unknown) throw new ArgumentException("Specified world cannot be Unknown", "world");

            DateTime cacheTime;
            if (element.Element("cache_info") != null)
                cacheTime = DateTime.Parse(element.Element("cache_info").Attribute("cached_until_gmt").Value, CultureInfo.InvariantCulture);
            else
                cacheTime = TimeZoneInfo.ConvertTimeToUtc(DateTime.Now);

            var fileName = string.Format(System.Globalization.CultureInfo.InvariantCulture, "{0}_{1}_{2}.xml", name, world.GetStringValue(), cacheTime.ToString(TimestampFormat, CultureInfo.InvariantCulture));

            SaveXmlToCache(fileName, element);
        }

        /// <summary>
        /// Saves the supplied XML document to the cache directory
        /// </summary>
        /// <param name="itemId">The item id.</param>
        /// <param name="element">The XML element.</param>
        public static void SaveXmlToCache(int itemId, XElement element)
        {
            if (itemId < 0) throw new ArgumentOutOfRangeException("itemId");
            if (element == null) throw new ArgumentNullException("element");

            DateTime cacheTime;
            if (element.Element("cache_info") != null)
                cacheTime = DateTime.Parse(element.Element("cache_info").Attribute("cached_until_gmt").Value, CultureInfo.InvariantCulture);
            else
                cacheTime = TimeZoneInfo.ConvertTimeToUtc(DateTime.Now);

            string fileName = string.Format(CultureInfo.InvariantCulture, "item_{0}_{1}.xml", itemId.ToString(CultureInfo.CurrentCulture), cacheTime.ToString(TimestampFormat, CultureInfo.InvariantCulture));

            SaveXmlToCache(fileName, element);
        }

        private static void SaveXmlToCache(string fileName, XElement element)
        {
            //If the cache path is invalid, cannot cache!
            if (!IsValidCacheLocation(CachePath))
                return;

            try
            {
                var savePath = Path.Combine(CachePath, fileName);
                var writer = XmlWriter.Create(savePath,
                    new XmlWriterSettings()
                    {
                        Indent = true,
                        CloseOutput = true
                    });

                element.Save(writer);

                writer.Flush();
                writer.Close();
            }
            catch (Exception)
            {
                throw;
            }

        }

        /// <summary>
        /// Deletes any cached XML files it find corresponding with the passed parameters.
        /// </summary>
        /// <param name="name">The name of the character or guild.</param>
        /// <param name="world">The world.</param>
        public static void DeleteFromCache(string name, LotroWorld world)
        {
            if (string.IsNullOrEmpty(name)) throw new ArgumentNullException("name");
            if (world == LotroWorld.Unknown) throw new ArgumentException("Specified world cannot be Unknown", "world");

            string fileName = string.Format(CultureInfo.InvariantCulture, "{0}_{1}*", name, world.GetStringValue());

            DeleteFromCache(fileName);
        }

        /// <summary>
        /// Deletes any cached XML files it find corresponding with the passed parameters.
        /// </summary>
        /// <param name="itemId">The item id.</param>
        public static void DeleteFromCache(int itemId)
        {
            if (itemId < 0) throw new ArgumentOutOfRangeException("itemId");

            string fileName = string.Format(CultureInfo.InvariantCulture, "item_{0}*", itemId.ToString(CultureInfo.CurrentCulture));

            DeleteFromCache(fileName);
        }

        private static void DeleteFromCache(string fileName)
        {
            var info = new DirectoryInfo(CachePath);

            var files = info.GetFiles(fileName);

            foreach (var file in files)
            {
                file.Delete();
            }
        }


        /// <summary>
        /// Loads the cached XML.
        /// </summary>
        /// <param name="name">The name, either character or guild.</param>
        /// <param name="world">The world.</param>
        /// <param name="ignoreTimestamp">if set to <c>true</c>, ignores the timestamp when loading.</param>
        /// <returns>An XElement.</returns>
        public static XElement LoadXmlFromCache(string name, LotroWorld world, bool ignoreTimestamp)
        {
            if (string.IsNullOrEmpty(name)) throw new ArgumentNullException("name");
            if (world == LotroWorld.Unknown) throw new ArgumentException("Specified world cannot be Unknown", "world");

            string fileName = string.Format(CultureInfo.InvariantCulture, "{0}_{1}*", name, world.GetStringValue());

            return LoadXmlFromCache(fileName, ignoreTimestamp);
        }

        /// <summary>
        /// Loads the chached XML.
        /// </summary>
        /// <param name="itemId">The item id.</param>
        /// <param name="ignoreTimestamp">if set to <c>true</c>, ignores the timestamp when loading.</param>
        /// <returns>An XElement.</returns>
        public static XElement LoadXmlFromCache(int itemId, bool ignoreTimestamp)
        {
            if (itemId < 0) throw new ArgumentOutOfRangeException("itemId");

            string fileName = string.Format(CultureInfo.InvariantCulture, "item_{0}*", itemId.ToString(CultureInfo.CurrentCulture));

            return LoadXmlFromCache(fileName, ignoreTimestamp);
        }

        private static XElement LoadXmlFromCache(string fileName, bool checkTimestamp)
        {
            if (!IsValidCacheLocation(CachePath))
                return null;

            try
            {
                var info = new DirectoryInfo(CachePath);

                var files = info.GetFiles(fileName);

                if (files.Length == 0)
                    return null;

                var cachedFile = files[0];

                //Do we want to check for a timestamp, items won't have this
                if (checkTimestamp)
                {
                    var timeStamp = ExtractTimeStamp(cachedFile.Name);

                    var gtmNow = TimeZoneInfo.ConvertTimeToUtc(DateTime.Now);

                    //If more than the cache time return null
                    if (gtmNow.Subtract(timeStamp) > CacheTime)
                    {
                        return null;
                    }
                }

                //Use the cached version
                return XElement.Load(cachedFile.FullName);
            }
            catch (IOException)
            { }

            //If it falls through to here, means an error has occured.
            return null;
        }



        /// <summary>
        /// Extracts the time stamp from the filename
        /// </summary>
        /// <param name="fileName">Name of the file.</param>
        /// <returns></returns>
        private static DateTime ExtractTimeStamp(string fileName)
        {
            var strParts = fileName.Split('_', '.');
            //3rd Argument will be timestamp
            return DateTime.ParseExact(strParts[2], TimestampFormat, System.Globalization.CultureInfo.InvariantCulture);
        }

        /// <summary>
        /// Determines whether [is valid cache location] [the specified path].
        /// </summary>
        /// <param name="path">The path.</param>
        /// <returns>
        /// 	<c>true</c> if [is valid cache location] [the specified path]; otherwise, <c>false</c>.
        /// </returns>
        private static bool IsValidCacheLocation(string path)
        {
            if (string.IsNullOrEmpty(path))
                return false;

            if (!Directory.Exists(path))
                return false;

            return true;
        }
    }
}
